Printing Graphics and Text

Printing from a Windows program usually involves more overhead than shown in the FORMFEED program, as well as some GDI calls to actually print something. Let's write a program that prints one page of text and graphics. We'll start with the method shown in the FORMFEED program and then add some enhancements. We'll be looking at three versions of this program called PRINT1, PRINT2, and PRINT3. To avoid a lot of duplicated source code, each of these programs will use the GETPRNDC.C file shown earlier and functions contained in the PRINT.C file, which is shown in Figure 13-6.

Figure 13-6. The PRINT.C file used in the PRINT1, PRINT2, and PRINT3 programs.

PRINT.C

/*-----------------------------------------------------------
   PRINT.C -- Common routines for Print1, Print2, and Print3
  -----------------------------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
BOOL PrintMyPage (HWND) ;

extern HINSTANCE hInst ;
extern TCHAR     szAppName[] ;
extern TCHAR     szCaption[] ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))

     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hInst = hInstance ;
     
     hwnd = CreateWindow (szAppName, szCaption,
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

void PageGDICalls (HDC hdcPrn, int cxPage, int cyPage)
{
     static TCHAR szTextStr[] = TEXT ("Hello, Printer!") ;
     
     Rectangle (hdcPrn, 0, 0, cxPage, cyPage) ;
     
     MoveToEx (hdcPrn, 0, 0, NULL) ;
     LineTo   (hdcPrn, cxPage, cyPage) ;
     MoveToEx (hdcPrn, cxPage, 0, NULL) ;
     LineTo   (hdcPrn, 0, cyPage) ;
     
     SaveDC (hdcPrn) ;
     
     SetMapMode       (hdcPrn, MM_ISOTROPIC) ;
     SetWindowExtEx   (hdcPrn, 1000, 1000, NULL) ;
     SetViewportExtEx (hdcPrn, cxPage / 2, -cyPage / 2, NULL) ;
     SetViewportOrgEx (hdcPrn, cxPage / 2,  cyPage / 2, NULL) ;
     
     Ellipse (hdcPrn, -500, 500, 500, -500) ;
     
     SetTextAlign (hdcPrn, TA_BASELINE | TA_CENTER) ;
     TextOut (hdcPrn, 0, 0, szTextStr, lstrlen (szTextStr)) ;
     RestoreDC (hdcPrn, -1) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static int   cxClient, cyClient ;
     HDC          hdc ;
     HMENU        hMenu ;
     PAINTSTRUCT  ps ;
     
     switch (message)
     {
     case WM_CREATE:
          hMenu = GetSystemMenu (hwnd, FALSE) ;
          AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
          AppendMenu (hMenu, 0, 1, TEXT ("&Print")) ;
          return 0 ;
          
     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;
          
     case WM_SYSCOMMAND:
          if (wParam == 1)
          {
               if (!PrintMyPage (hwnd))
                    MessageBox (hwnd, TEXT ("Could not print page!"),
                                szAppName, MB_OK | MB_ICONEXCLAMATION) ;
               return 0 ;
          }
          break ;
          
     case WM_PAINT :
          hdc = BeginPaint (hwnd, &ps) ;
          
          PageGDICalls (hdc, cxClient, cyClient) ;
          
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DESTROY :
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

PRINT.C contains the functions WinMain and WndProc, and also a function called PageGDICalls, which expects to receive a handle to the printer device context and two variables containing the width and height of the printer page. PageGDICalls draws a rectangle that encompasses the entire page, two lines between opposite corners of the page, an ellipse in the middle of the page (its diameter half the lesser of the printer height and width), and the text "Hello, Printer!" in the center of this ellipse.

During processing of the WM_CREATE message, WndProc adds a Print option to the system menu. Selecting this option causes a call to PrintMyPage, a function that we'll enhance over the course of the three versions of the program. PrintMyPage returns TRUE if it successfully prints the page and FALSE if it encounters an error during printing. If PrintMyPage returns FALSE, WndProc displays a message box to inform you of the error.

Bare-Bones Printing

PRINT1, the first version of the printing program, is shown in Figure 13-7. After compiling PRINT1, you can execute it and then select Print from the system menu. In quick succession, GDI saves the necessary printer output in a temporary file, and then the spooler sends it to the printer.

Figure 13-7. The PRINT1 program.

PRINT1.C

/*---------------------------------------
   PRINT1.C -- Bare Bones Printing
               (c) Charles Petzold, 1998
  ---------------------------------------*/

#include <windows.h>

HDC  GetPrinterDC (void) ;              // in GETPRNDC.C
void PageGDICalls (HDC, int, int) ;     // in PRINT.C

HINSTANCE hInst ;
TCHAR     szAppName[] = TEXT ("Print1") ;
TCHAR     szCaption[] = TEXT ("Print Program 1") ;

BOOL PrintMyPage (HWND hwnd)
{
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print1: Printing") } ;
     BOOL           bSuccess = TRUE ;
     HDC            hdcPrn ;
     int            xPage, yPage ;
     
     if (NULL == (hdcPrn = GetPrinterDC ()))
          return FALSE ;

     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;
     
     if (StartDoc (hdcPrn, &di) > 0)
     {
          if (StartPage (hdcPrn) > 0)
          {
               PageGDICalls (hdcPrn, xPage, yPage) ;
               
               if (EndPage (hdcPrn) > 0)
                    EndDoc (hdcPrn) ;
               else
                    bSuccess = FALSE ;
          }
     }
     else
          bSuccess = FALSE ;
     
     DeleteDC (hdcPrn) ;
     return bSuccess ;
}

Let's look at the code in PRINT1.C. If PrintMyPage can't obtain a device context handle for the printer, it returns FALSE and WndProc displays the message box indicating an error. If the function succeeds in obtaining the device context handle, it then determines the horizontal and vertical size of the page in pixels by calling GetDeviceCaps:

xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
yPage = GetDeviceCaps (hdcPrn, VERTRES) ;

This is not the full size of the paper but rather its printable area. After that call, the code in PRINT1's PrintMyPage function is structurally the same as the code in FORMFEED, except that PRINT1 calls PageGDICalls between the StartPage and EndPage calls. Only if the calls to StartDoc, StartPage, and EndPage are successful does PRINT1 call the EndDoc print function.

Canceling Printing with an Abort Procedure

For large documents, a program should provide the user with a convenient way to cancel a print job while the application is still printing. Perhaps the user intended to print only one page of a document but instead elected to print all 537 pages. That should be a mistake that is correctable before all 537 pages have printed.

Canceling a print job from within an application requires something called an "abort procedure." The abort procedure is a small exported function in your program. You give Windows the address of this function as an argument to the SetAbortProc function; GDI then calls the procedure repeatedly during printing, in essence asking, "Shall I continue printing?"

Let's look first at what's required to add an abort procedure to the printing logic and then examine some of the ramifications. The abort procedure is commonly called AbortProc, and it takes the following form:

BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)
{

     [other program lines]
}

Before printing, you must register the abort procedure by calling SetAbortProc:

SetAbortProc (hdcPrn, AbortProc) ;

You make this call before the StartDoc call. You don't need to "unset" the abort procedure after you finish printing.

While processing the EndPage call (that is, while playing the metafile into the device driver and creating the temporary printer output files), GDI frequently calls the abort procedure. The hdcPrn parameter is the printer device context handle. The iCode parameter is 0 if all is going well or is SP_OUTOFDISK if the GDI module has run out of disk space because of the temporary printer output files.

AbortProc must return TRUE (nonzero) if the print job is to be continued and returns FALSE (0) if the print job is to be aborted. The abort procedure can be as simple as this:

BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)
{
     MSG   msg ;

     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return TRUE ;
}

This function may seem a little peculiar. In fact, it looks suspiciously like a message loop. What's a message loop doing here of all places? Well, it is a message loop. You'll note, however, that this message loop calls PeekMessage rather than GetMessage. I discussed PeekMessage in connection with the RANDRECT program at the end of Chapter 5. You'll recall that PeekMessage returns control to a program with a message from the program's message queue (just like GetMessage) but also returns control if there are no messages waiting in any program's message queue.

The message loop in the AbortProc function repeatedly calls PeekMessage while PeekMessage returns TRUE. This TRUE value means that PeekMessage has retrieved a message that can be sent to one of the program's window procedures using TranslateMessage and DispatchMessage. When there are no more messages in the program's message queue, the return value of PeekMessage is then FALSE, so AbortProc returns control to Windows.

How Windows Uses AbortProc

When a program is printing, the bulk of the work takes place during the call to EndPage. Before that call, the GDI module simply adds another record to the disk-based metafile every time the program calls a GDI drawing function. When GDI gets the EndPage call, it plays this metafile into the device driver once for each band the device driver defines on a page. GDI then stores in a file the printer output created by the printer driver. If the spooler isn't active, the GDI module itself must write this printer output to the printer.

During the call to EndPage, the GDI module calls the abort procedure you've set. Normally, the iCode parameter is 0, but if GDI has run out of disk space because of the presence of other temporary files that haven't been printed yet, the iCode parameter is SP_OUTOFDISK. (You wouldn't normally check this value, but you can if you want.) The abort procedure then goes into its PeekMessage loop to retrieve messages from the program's message queue.

If there are no messages in the program's message queue, PeekMessage returns FALSE. The abort procedure then drops out of its message loop and returns a TRUE value to the GDI module to indicate that printing should continue. The GDI module then continues to process the EndPage call.

The GDI module stops the print process if an error occurs, so the main purpose of the abort procedure is to allow the user to cancel printing. For that we also need a dialog box that displays a Cancel button. Let's take these two steps one at a time. First we'll add an abort procedure to create the PRINT2 program, and then we'll add a dialog with a Cancel button in PRINT3 to make the abort procedure useful.

Implementing an Abort Procedure

Let's quickly review the mechanics of the abort procedure. You define an abort procedure that looks like this:

BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)
{
     MSG  msg ;

     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return TRUE ;
}

To print something, you give Windows a pointer to the abort procedure:

SetAbortProc (hdcPrn, AbortProc) ;

You make this call before the StartDoc call. And that's it.

Well, not quite. We've overlooked a problem with that PeekMessage loop in AbortProc—a big problem. AbortProc is called only while your program is in the midst of printing. Some very ugly things can happen if you retrieve a message in AbortProc and dispatch it to your own window procedure. A user could select Print from the menu again. But the program is already in the middle of the printing routine. A user could load a new file into the program while the program is trying to print the previous file. A user could even quit your program! If that happens, all your program's windows will be destroyed. You'll eventually return from the printing routine, but you'll have nowhere to go except to a window procedure that's no longer valid.

This stuff boggles the mind. And your program isn't prepared for it. For this reason, when you set an abort procedure, you should first disable your program's window so that it can't receive keyboard and mouse input. You do this with

EnableWindow (hwnd, FALSE) ;

This prevents keyboard and mouse input from getting into the message queue. The user therefore can't do anything with your program during the time it's printing. When printing is finished, you reenable the window for input:

EnableWindow (hwnd, TRUE) ;

So why, you ask, do we even bother with the TranslateMessage and DispatchMessage calls in AbortProc when no keyboard or mouse messages will get into the message queue in the first place? It's true that the TranslateMessage call isn't strictly needed (although it's almost always included). But we must use DispatchMessage in case a WM_PAINT message gets in the message queue. If WM_PAINT isn't processed properly with a BeginPaint and EndPaint pair in the window procedure, the message will remain in the queue and clog up the works, because PeekMessage will never return a FALSE.

When you disable your window during the time you're printing, your program remains inert on the display. But a user can switch to another program and do some work there, and the spooler can continue sending output files to the printer.

The PRINT2 program, shown in Figure 13-8, adds to PRINT1 an abort procedure and the necessary support—a call to the AbortProc function and two calls to EnableWindow, the first to disable the window and the second to reenable it.

Figure 13-8. The PRINT2 program.

PRINT2.C

/*-------------------------------------------
   PRINT2.C -- Printing with Abort Procedure
               (c) Charles Petzold, 1998
  -------------------------------------------*/


#include <windows.h>

HDC  GetPrinterDC (void) ;              // in GETPRNDC.C
void PageGDICalls (HDC, int, int) ;     // in PRINT.C

HINSTANCE hInst ;
TCHAR     szAppName[] = TEXT ("Print2") ;
TCHAR     szCaption[] = TEXT ("Print Program 2 (Abort Procedure)") ;

BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)
{
     MSG msg ;
     
     while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return TRUE ;
}

BOOL PrintMyPage (HWND hwnd)
{
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print2: Printing") } ;
     BOOL           bSuccess = TRUE ;
     HDC            hdcPrn ;
     short          xPage, yPage ;
     
     if (NULL == (hdcPrn = GetPrinterDC ()))
          return FALSE ;
     
     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;
     
     EnableWindow (hwnd, FALSE) ;
     
     SetAbortProc (hdcPrn, AbortProc) ;
     
     if (StartDoc (hdcPrn, &di) > 0)

     {
          if (StartPage (hdcPrn) > 0)
          {
               PageGDICalls (hdcPrn, xPage, yPage) ;
               
               if (EndPage (hdcPrn) > 0)
                    EndDoc (hdcPrn) ;
               else
                    bSuccess = FALSE ;
          }
     }
     else
          bSuccess = FALSE ;
     
     EnableWindow (hwnd, TRUE) ;
     DeleteDC (hdcPrn) ;
     return bSuccess ;
}

Adding a Printing Dialog Box

PRINT2 is not entirely satisfactory. First, the program doesn't directly indicate when it is printing and when it is finished with printing. Only when you poke at the program with the mouse and find that it doesn't respond can you determine that it must still be processing the PrintMyPage routine. Nor does PRINT2 give the user the opportunity to cancel the print job while it is spooling.

You're probably aware that most Windows programs give users a chance to cancel a printing operation currently in progress. A small dialog box comes up on the screen; it contains some text and a push button labeled Cancel. The program displays this dialog box during the entire time that GDI is saving the printer output in a disk file or (if the spooler is disabled) while the printer is printing. This is a modeless dialog box, and you must supply the dialog procedure.

This dialog box is often called the "abort dialog box," and the dialog procedure is often called the "abort dialog procedure." To distinguish it more clearly from the "abort procedure," I'll call this dialog procedure the "printing dialog procedure." The abort procedure (with the name AbortProc) and the printing dialog procedure (which I'll name PrintDlgProc) are two separate exported functions. If you want to print in a professional, Windows-like manner, you must have both of these.

These two functions interact as follows. The PeekMessage loop in AbortProc must be modified to send messages for the modeless dialog box to the dialog box window procedure. PrintDlgProc must process WM_COMMAND messages to check the status of the Cancel button. If the Cancel button is pressed, it sets a global variable called bUserAbort to TRUE. The value returned from AbortProc is the inverse of bUserAbort. You will recall that AbortProc returns TRUE to continue printing and FALSE to abort printing. In PRINT2 we always returned TRUE. Now we'll return FALSE if the user clicks the Cancel button in the printing dialog box. This logic is implemented in the PRINT3 program, shown in Figure 13-9.

Figure 13-9. The PRINT3 program.

PRINT3.C

/*---------------------------------------
   PRINT3.C -- Printing with Dialog Box
               (c) Charles Petzold, 1998
  ---------------------------------------*/

#include <windows.h>

HDC  GetPrinterDC (void) ;              // in GETPRNDC.C
void PageGDICalls (HDC, int, int) ;     // in PRINT.C

HINSTANCE hInst ;
TCHAR     szAppName[] = TEXT ("Print3") ;
TCHAR     szCaption[] = TEXT ("Print Program 3 (Dialog Box)") ;

BOOL bUserAbort ;
HWND hDlgPrint ;

BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT message, 
                            WPARAM wParam, LPARAM lParam)
{
     switch (message)
     {
     case WM_INITDIALOG:
          SetWindowText (hDlg, szAppName) ;
          EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE, MF_GRAYED) ;
          return TRUE ;
          
     case WM_COMMAND:
          bUserAbort = TRUE ;
          EnableWindow (GetParent (hDlg), TRUE) ;
          DestroyWindow (hDlg) ;
          hDlgPrint = NULL ;
          return TRUE ;
     }
     return FALSE ;
}

BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode)

{
     MSG msg ;
     
     while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
          if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
          {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
          }
     }
     return !bUserAbort ;
}

BOOL PrintMyPage (HWND hwnd)
{
     static DOCINFO di = { sizeof (DOCINFO), TEXT ("Print3: Printing") } ;
     BOOL           bSuccess = TRUE ;
     HDC            hdcPrn ;
     int            xPage, yPage ;
     
     if (NULL == (hdcPrn = GetPrinterDC ()))
          return FALSE ;
     
     xPage = GetDeviceCaps (hdcPrn, HORZRES) ;
     yPage = GetDeviceCaps (hdcPrn, VERTRES) ;
     
     EnableWindow (hwnd, FALSE) ;
     
     bUserAbort = FALSE ;
     hDlgPrint = CreateDialog (hInst, TEXT ("PrintDlgBox"), 
                               hwnd, PrintDlgProc) ;
     
     SetAbortProc (hdcPrn, AbortProc) ;
     
     if (StartDoc (hdcPrn, &di) > 0)
     {
          if (StartPage (hdcPrn) > 0)
          {
               PageGDICalls (hdcPrn, xPage, yPage) ;
               
               if (EndPage (hdcPrn) > 0)
                    EndDoc (hdcPrn) ;
               else
                    bSuccess = FALSE ;
          }
     }
     else
          bSuccess = FALSE ;

     if (!bUserAbort)
     {
          EnableWindow (hwnd, TRUE) ;
          DestroyWindow (hDlgPrint) ;
     }
     
     DeleteDC (hdcPrn) ;
     
     return bSuccess && !bUserAbort ;
}

PRINT.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Dialog

PRINTDLGBOX DIALOG DISCARDABLE  20, 20, 186, 63
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
FONT 8, "MS Sans Serif"
BEGIN
    PUSHBUTTON      "Cancel",IDCANCEL,67,42,50,14
    CTEXT           "Cancel Printing",IDC_STATIC,7,21,172,8
END

If you experiment with PRINT3, you may want to temporarily disable print spooling. Otherwise, the Cancel button, which is visible only while the spooler collects data from PRINT3, might disappear too quickly for you to actually click on it. Don't be surprised if things don't come to an immediate halt when you click the Cancel button, especially on a slow printer. The printer has an internal buffer that must drain before the printer stops. Clicking Cancel merely tells GDI not to send any more data to the printer's buffer.

Two global variables are added to PRINT3: a BOOL called bUserAbort and a handle to the dialog box window called hDlgPrint. The PrintMyPage function initializes bUserAbort to FALSE, and as in PRINT2, the program's main window is disabled. The pointer to AbortProc is used in the SetAbortProc call, and the pointer to PrintDlgProc is used in a CreateDialog call. The window handle returned from CreateDialog is saved in hDlgPrint.

The message loop in AbortProc now looks like this:

while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
{
     if (!hDlgPrint ¦¦ !IsDialogMessage (hDlgPrint, &msg))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
}
return !bUserAbort ;

It calls PeekMessage only if bUserAbort is FALSE—that is, if the user hasn't yet aborted the printing operation. The IsDialogMessage function is required to send the message to the modeless dialog box. As is normal with modeless dialog boxes, the handle to the dialog box window is checked before this call is made. AbortProc returns the inverse of bUserAbort. Initially, bUserAbort is FALSE, so AbortProc returns TRUE, indicating that printing is to continue. But bUserAbort could be set to TRUE in the printing dialog procedure.

The PrintDlgProc function is fairly simple. While processing WM_INITDIALOG, the function sets the window caption to the name of the program and disables the Close option on the system menu. If the user clicks the Cancel button, PrintDlgProc receives a WM_ COMMAND message:

case WM_COMMAND :
     bUserAbort = TRUE ;
     EnableWindow (GetParent (hDlg), TRUE) ;
     DestroyWindow (hDlg) ;
     hDlgPrint = NULL ;
     return TRUE ;

Setting bUserAbort to TRUE indicates that the user has decided to cancel the printing operation. The main window is enabled, and the dialog box is destroyed. (It is important that you perform these two actions in this order. Otherwise, some other program running under Windows will become the active program, and your program might disappear into the background.) As is normal, hDlgPrint is set to NULL to prevent IsDialogMessage from being called in the message loop.

The only time this dialog box receives messages is when AbortProc retrieves messages with PeekMessage and sends them to the dialog box window procedure with IsDialogMessage. The only time AbortProc is called is when the GDI module is processing the EndPage function. If GDI sees that the return value from AbortProc is FALSE, it returns control from the EndPage call back to PrintMyPage. It doesn't return an error code. At that point, PrintMyPage thinks that the page is complete and calls the EndDoc function. Nothing is printed, however, because the GDI module didn't finish processing the EndPage call.

Some cleanup remains. If the user didn't cancel the print job from the dialog box, then the dialog box is still displayed. PrintMyPage reenables its main window and destroys the dialog box:

if (!bUserAbort)
{
     EnableWindow (hwnd, TRUE) ;
     DestroyWindow (hDlgPrint) ;
}

Two variables tell you what happened: bUserAbort tells you whether the user aborted the print job, and bSuccess tells you whether an error occurred. You can do what you want with these variables. PrintMyPage simply performs a logical AND operation to return to WndProc:

return bSuccess && !bUserAbort ;

Adding Printing to POPPAD

Now we're ready to add a printing facility to the POPPAD series of programs and declare POPPAD finished. You'll need the various POPPAD files from Chapter 11, plus the POPPRNT.C file in Figure 13-10.

Figure 13-10. The POPPRNT.C file to add printing capability to POPPAD.

POPPRNT.C

/*----------------------------------------------
   POPPRNT.C -- Popup Editor Printing Functions
  ----------------------------------------------*/

#include <windows.h>
#include <commdlg.h>
#include "resource.h"

BOOL bUserAbort ;
HWND hDlgPrint ;

BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
     switch (msg)
     {
     case WM_INITDIALOG :
          EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC_CLOSE, MF_GRAYED) ;
          return TRUE ;
          
     case WM_COMMAND :
          bUserAbort = TRUE ;
          EnableWindow (GetParent (hDlg), TRUE) ;
          DestroyWindow (hDlg) ;

          hDlgPrint = NULL ;
          return TRUE ;
     }
     return FALSE ;
}          

BOOL CALLBACK AbortProc (HDC hPrinterDC, int iCode)
{
     MSG msg ;
     
     while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
     {
          if (!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg))
          {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
          }
     }
     return !bUserAbort ;
}

BOOL PopPrntPrintFile (HINSTANCE hInst, HWND hwnd, HWND hwndEdit, 
                       PTSTR szTitleName)
{
     static DOCINFO  di = { sizeof (DOCINFO) } ;
     static PRINTDLG pd ;
     BOOL            bSuccess ;
     int             yChar, iCharsPerLine, iLinesPerPage, iTotalLines,
                     iTotalPages, iPage, iLine, iLineNum ;
     PTSTR           pstrBuffer ;
     TCHAR           szJobName [64 + MAX_PATH] ;
     TEXTMETRIC      tm ;
     WORD            iColCopy, iNoiColCopy ;

          // Invoke Print common dialog box
     
     pd.lStructSize         = sizeof (PRINTDLG) ;
     pd.hwndOwner           = hwnd ;
     pd.hDevMode            = NULL ;
     pd.hDevNames           = NULL ;
     pd.hDC                 = NULL ;
     pd.Flags               = PD_ALLPAGES | PD_COLLATE | 
                              PD_RETURNDC | PD_NOSELECTION ;
     pd.nFromPage           = 0 ;
     pd.nToPage             = 0 ;
     pd.nMinPage            = 0 ;
     pd.nMaxPage            = 0 ;
     pd.nCopies             = 1 ;
     pd.hInstance           = NULL ;
     pd.lCustData           = 0L ;
     pd.lpfnPrintHook       = NULL ;
     pd.lpfnSetupHook       = NULL ;
     pd.lpPrintTemplateName = NULL ;
     pd.lpSetupTemplateName = NULL ;
     pd.hPrintTemplate      = NULL ;
     pd.hSetupTemplate      = NULL ;
     
     if (!PrintDlg (&pd))
          return TRUE ;
     
     if (0 == (iTotalLines = SendMessage (hwndEdit, EM_GETLINECOUNT, 0, 0)))
          return TRUE ;

          // Calculate necessary metrics for file 
     
     GetTextMetrics (pd.hDC, &tm) ;
     yChar = tm.tmHeight + tm.tmExternalLeading ;
     
     iCharsPerLine = GetDeviceCaps (pd.hDC, HORZRES) / tm.tmAveCharWidth ;
     iLinesPerPage = GetDeviceCaps (pd.hDC, VERTRES) / yChar ;
     iTotalPages   = (iTotalLines + iLinesPerPage - 1) / iLinesPerPage ;

          // Allocate a buffer for each line of text
     
     pstrBuffer = malloc (sizeof (TCHAR) * (iCharsPerLine + 1)) ;

          // Display the printing dialog box
     
     EnableWindow (hwnd, FALSE) ;
     
     bSuccess   = TRUE ;
     bUserAbort = FALSE ;
     
     hDlgPrint = CreateDialog (hInst, TEXT ("PrintDlgBox"), 
                               hwnd, PrintDlgProc) ;

     SetDlgItemText (hDlgPrint, IDC_FILENAME, szTitleName) ;
     SetAbortProc (pd.hDC, AbortProc) ;

          // Start the document

     GetWindowText (hwnd, szJobName, sizeof (szJobName)) ;
     di.lpszDocName = szJobName ;

     if (StartDoc (pd.hDC, &di) > 0)
     {
               // Collation requires this loop and iNoiColCopy

          for (iColCopy = 0 ;
               iColCopy < ((WORD) pd.Flags & PD_COLLATE ? pd.nCopies : 1) ;
               iColCopy++)
          {
               for (iPage = 0 ; iPage < iTotalPages ; iPage++)
               {
                    for (iNoiColCopy = 0 ;
                         iNoiColCopy < (pd.Flags & PD_COLLATE ? 1 : pd.nCopies);
                         iNoiColCopy++)
                    {
                              // Start the page

                         if (StartPage (pd.hDC) < 0)
                         {
                              bSuccess = FALSE ;
                              break ;
                         }

                              // For each page, print the lines
                         
                         for (iLine = 0 ; iLine < iLinesPerPage ; iLine++)
                         {
                              iLineNum = iLinesPerPage * iPage + iLine ;
                              
                              if (iLineNum > iTotalLines)
                                   break ;
                              
                              *(int *) pstrBuffer = iCharsPerLine ;
                              
                              TextOut (pd.hDC, 0, yChar * iLine, pstrBuffer,
                                       (int) SendMessage (hwndEdit, EM_GETLINE,
                                       (WPARAM) iLineNum, (LPARAM) pstrBuffer));
                         }
                         
                         if (EndPage (pd.hDC) < 0)
                         {
                              bSuccess = FALSE ;
                              break ;
                         }
                         
                         if (bUserAbort)
                              break ;
                    }
                    
                    if (!bSuccess || bUserAbort)
                         break ;
               }
               
               if (!bSuccess || bUserAbort)
                    break ;
          }
     }
     else
          bSuccess = FALSE ;
     
     if (bSuccess)
          EndDoc (pd.hDC) ;
     
     if (!bUserAbort)
     {
          EnableWindow (hwnd, TRUE) ;
          DestroyWindow (hDlgPrint) ;
     }
     
     free (pstrBuffer) ;
     DeleteDC (pd.hDC) ;
     
     return bSuccess && !bUserAbort ;
}

In keeping with the philosophy of making POPPAD as simple as possible by taking advantage of high-level Windows features, the POPPRNT.C file demonstrates how to use the PrintDlg function. This function is included in the common dialog box library and uses a structure of type PRINTDLG.

Normally, a Print option is included on a program's File menu. When the user selects the Print option, a program can initialize the fields of the PRINTDLG structure and call PrintDlg.

PrintDlg displays a dialog box that allows the user to select a page range to print. Thus, this dialog box is particularly suitable for programs such as POPPAD that can print multipage documents. The dialog box also provides an edit field to specify the number of copies and a check-box labeled "Collate." Collation affects the page ordering of multiple copies. For example, if the document is three pages long and the user requests that three copies be printed, the program can print them in one of two orders. Collated copies are in the page order 1, 2, 3, 1, 2, 3, 1, 2, 3. Noncollated copies are in the order 1, 1, 1, 2, 2, 2, 3, 3, 3. It's up to your program to print the copies in the correct order.

The dialog box also allows the user to select a nondefault printer, and it includes a button labeled Properties that invokes a device mode dialog box. At the very least, this allows the user to select portrait or landscape mode.

On return from the PrintDlg function, fields of the PRINTDLG structure indicate the range of pages to print and whether multiple copies should be collated. The structure also provides the printer device context handle, ready to be used.

In POPPRNT.C, the PopPrntPrintFile function (which is called from POPPAD when the user selects the Print option from the File menu) calls PrintDlg and then proceeds to print the file. PopPrntPrintFile then performs some calculations to determine the number of characters it can fit on a line and the number of lines it can fit on a page. This process involves calls to GetDeviceCaps to determine the resolution of the page and to GetTextMetrics for the dimensions of a character.

The program obtains the total number of lines in the document (the variable iTotalLines) by sending an EM_GETLINECOUNT message to the edit control. A buffer for holding the contents of each line is allocated from local memory. For each line, the first word of this buffer is set to the number of characters in the line. Sending the edit control an EM_GETLINE message copies a line into the buffer; the line is then sent to the printer device context using TextOut. (POPPRNT.C is not smart enough to wrap lines that exceed the width of the printer page. We'll examine a technique for wrapping such lines in Chapter 17.)

Notice that the logic to print the document includes two for loops for the number of copies. The first uses a variable named iColCopy and takes effect when the user has specified collated copies; the second uses the iNonColCopy variable and takes effect for noncollated copies.

The program breaks from the for loop incrementing the page number if either StartPage or EndPage returns an error or if bUserAbort is TRUE. If the return value of the abort procedure is FALSE, EndPage doesn't return an error. For this reason, bUserAbort is tested explicitly before the next page is started. If no error is reported, the call to EndDoc is made:

if (!bError)
     EndDoc (hdcPrn) ;

You might want to experiment with POPPAD by printing a multipage file. You can monitor progress from the print job window. The file being printed first shows up in this window after GDI has finished processing the first EndPage call. At that time, the spooler starts sending the file to the printer. If you then cancel the print job from POPPAD, the spooler aborts the printing also—that's a result of returning FALSE from the abort procedure. Once the file appears in the print job window, you can also cancel the printing by selecting Cancel Printing from the Document menu. In that case, the EndPage call in progress in POPPAD returns an error.

Programmers new to Windows often become inordinately obsessed with the AbortDoc function. This function is rarely used in printing. As you can see in POPPAD, a user can cancel a print job at almost any time, either through POPPAD's printing dialog box or through the print job window. Neither requires that the program use the AbortDoc function. The only time that AbortDoc would be allowed in POPPAD is between the call to StartDoc and the first call to EndPage, but that code goes so quickly that AbortDoc isn't necessary.

Figure 13-11 shows the correct sequence of print function calls for printing a multipage document. The best place to check for a bUserAbort value of TRUE is after each call to EndPage. The EndDoc function is used only when the previous print functions have proceeded without error. In fact, once you get an error from any call to a print function, the show is over and you can go home.

Figure 13-11. The sequence of function calls for multipage printing.